#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import wsgiref.handlers
import datetime
import string
import logging
import cssutils
import urllib
import re
from django.utils import simplejson
from google.appengine.api import memcache
from google.appengine.api import urlfetch
from google.appengine.ext import webapp

# for jsmin
from StringIO import StringIO

# JSMIN by Doug Cockford
# http://www.crockford.com/javascript/jsmin.html
# specifically, http://www.crockford.com/javascript/jsmin.py.txt
def jsmin(js):
    ins = StringIO(js)
    outs = StringIO()
    JavascriptMinify().minify(ins, outs)
    str = outs.getvalue()
    if len(str) > 0 and str[0] == '\n':
        str = str[1:]
    return str

def replaceUrls(stylesheet, orig_url):
  """replace URLs with fully qualified URLs-- as a proxy, relative
  URL paths will resolve incorrectly."""
  base_url = re.sub(r'[?].*$', '', orig_url)
  # http://x/y/z.ext => http://x/
  base = re.sub(r'^(https?://[^/]+/).*$', r'\1', base_url)
  # http://x/y/z.ext => http://x/y/
  base_with_path = re.sub(r'/[^/]*$', '/', base_url)
  def replacer(url):
    if re.search(r'^https?://', url):
      return url
    if url[0] == '/':
      return base + url[1:]
    return base_with_path + url
  cssutils.replaceUrls(stylesheet, replacer)

# cssutils
# http://code.google.com/p/cssutils/
def cssmin(orig_url, str):
    stylesheet = cssutils.parseString(str)
    replaceUrls(stylesheet, orig_url)
    cssutils.ser.prefs.useMinified()
    return stylesheet.cssText

def isAlphanum(c):
    """return true if the character is a letter, digit, underscore,
           dollar sign, or non-ASCII character.
    """
    return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
            (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));

class UnterminatedComment(Exception):
    pass

class UnterminatedStringLiteral(Exception):
    pass

class UnterminatedRegularExpression(Exception):
    pass

class JavascriptMinify(object):

    def _outA(self):
        self.outstream.write(self.theA)
    def _outB(self):
        self.outstream.write(self.theB)

    def _get(self):
        """return the next character from stdin. Watch out for lookahead. If
           the character is a control character, translate it to a space or
           linefeed.
        """
        c = self.theLookahead
        self.theLookahead = None
        if c == None:
            c = self.instream.read(1)
        if c >= ' ' or c == '\n':
            return c
        if c == '': # EOF
            return '\000'
        if c == '\r':
            return '\n'
        return ' '

    def _peek(self):
        self.theLookahead = self._get()
        return self.theLookahead

    def _next(self):
        """get the next character, excluding comments. peek() is used to see
           if a '/' is followed by a '/' or '*'.
        """
        c = self._get()
        if c == '/':
            p = self._peek()
            if p == '/':
                c = self._get()
                while c > '\n':
                    c = self._get()
                return c
            if p == '*':
                c = self._get()
                while 1:
                    c = self._get()
                    if c == '*':
                        if self._peek() == '/':
                            self._get()
                            return ' '
                    if c == '\000':
                        raise UnterminatedComment()

        return c

    def _action(self, action):
        """do something! What you do is determined by the argument:
           1   Output A. Copy B to A. Get the next B.
           2   Copy B to A. Get the next B. (Delete A).
           3   Get the next B. (Delete B).
           action treats a string as a single character. Wow!
           action recognizes a regular expression if it is preceded by ( or , or =.
        """
        if action <= 1:
            self._outA()

        if action <= 2:
            self.theA = self.theB
            if self.theA == "'" or self.theA == '"':
                while 1:
                    self._outA()
                    self.theA = self._get()
                    if self.theA == self.theB:
                        break
                    if self.theA <= '\n':
                        raise UnterminatedStringLiteral()
                    if self.theA == '\\':
                        self._outA()
                        self.theA = self._get()


        if action <= 3:
            self.theB = self._next()
            if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
                                     self.theA == '=' or self.theA == ':' or
                                     self.theA == '[' or self.theA == '?' or
                                     self.theA == '!' or self.theA == '&' or
                                     self.theA == '|' or self.theA == ';' or
                                     self.theA == '{' or self.theA == '}' or
                                     self.theA == '\n'):
                self._outA()
                self._outB()
                while 1:
                    self.theA = self._get()
                    if self.theA == '/':
                        break
                    elif self.theA == '\\':
                        self._outA()
                        self.theA = self._get()
                    elif self.theA <= '\n':
                        raise UnterminatedRegularExpression()
                    self._outA()
                self.theB = self._next()


    def _jsmin(self):
        """Copy the input to the output, deleting the characters which are
           insignificant to JavaScript. Comments will be removed. Tabs will be
           replaced with spaces. Carriage returns will be replaced with linefeeds.
           Most spaces and linefeeds will be removed.
        """
        self.theA = '\n'
        self._action(3)

        while self.theA != '\000':
            if self.theA == ' ':
                if isAlphanum(self.theB):
                    self._action(1)
                else:
                    self._action(2)
            elif self.theA == '\n':
                if self.theB in ['{', '[', '(', '+', '-']:
                    self._action(1)
                elif self.theB == ' ':
                    self._action(3)
                else:
                    if isAlphanum(self.theB):
                        self._action(1)
                    else:
                        self._action(2)
            else:
                if self.theB == ' ':
                    if isAlphanum(self.theA):
                        self._action(1)
                    else:
                        self._action(3)
                elif self.theB == '\n':
                    if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
                        self._action(1)
                    else:
                        if isAlphanum(self.theA):
                            self._action(1)
                        else:
                            self._action(3)
                else:
                    self._action(1)

    def minify(self, instream, outstream):
        self.instream = instream
        self.outstream = outstream
        self.theA = '\n'
        self.theB = None
        self.theLookahead = None

        self._jsmin()
        self.instream.close()


class MainHandler(webapp.RequestHandler):
  def get(self, hardcoded_url=None):
    nocache = (self.request.get("nocache") == "true")
    do_jsmin = (self.request.get("jsmin") == "true")
    do_cssmin = (self.request.get("cssmin") == "true")
    mimetype = self.request.get("mime")
    # TODO: disallow both jsmin and cssmin
    output = (self.request.get("output"))
    callback = (self.request.get("callback"))
    if hardcoded_url:
      urls = [urllib.unquote(hardcoded_url)]
      logging.info(urls[0])
    else:
      urls = self.request.get_all("url")

    if not mimetype:
      mimetype = ""
    if do_jsmin:
      mimetype = "text/javascript"
    elif do_cssmin:
      mimetype = "text/css"
    elif mimetype == "js":
      mimetype = "text/javascript"
    elif mimetype == "css":
      # explicit control in case cssmin isn't being used
      mimetype = "text/css"
    elif mimetype == "" and len(urls) == 1:
      if re.search(r'[.]css$', urls[0], re.IGNORECASE):
        mimetype = "text/css"
      elif re.search(r'[.]js$', urls[0], re.IGNORECASE):
        mimetype = "text/javascript"
      elif re.search(r'[.]swf$', urls[0], re.IGNORECASE):
        mimetype = "application/x-shockwave-flash"
      elif re.search(r'[.]jpe?g$', urls[0], re.IGNORECASE):
        mimetype = "image/jpeg"
      elif len(urls) == 1 and re.search(r'[.]png$', urls[0], re.IGNORECASE):
        mimetype = "image/png"
      elif len(urls) == 1 and re.search(r'[.]gif$', urls[0], re.IGNORECASE):
        mimetype = "image/gif"
    if mimetype != "":
      self.response.headers["Content-Type"] = mimetype
    else:
      self.response.out.write("invalid request-- please provide mimetype")
      return

    contents = []
    index = 0
    for url in urls:
      from_cache = True
      fetch_time = 0
      data = memcache.get(url)
      if nocache or data is None:
        logging.info("fetching "+url)
        from_cache = False
        try:
          start = datetime.datetime.utcnow()  # We time the fetch for info purposes
          response = urlfetch.fetch(url)
          fetch_time = getMillis(datetime.datetime.utcnow() - start)
        except urlfetch.InvalidURLError:
          self.print_error("Invalid URL: <b>%s</b>" % (url))
          return
        except urlfetch.ResponseTooLarge:
          self.print_error("Request file size too large: <b>%s</b>" % (url))
          return
        
        if response.status_code != 200:
          self.print_error("Requesting <b>%s</b> returned status code: <b>%d</b>" %
                           (url, response.status_code))
          return
        
        # TODO: Allow configurable cache times?
        data = response.content
        memcache.add(url, data, 86400)  # 24 hours

      # run minimizers after caching, i.e. don't cache minimized content
      if do_cssmin:
        data = cssmin(url, data)
      elif mimetype == "css":
        # have to parse/print CSS even when not minimizing, so that we can
        # fully qualify URLs.
        cssutils.ser.prefs.useDefaults()
        stylesheet = cssutils.parseString(data)
        replaceUrls(stylesheet, self.request.url)
        data = stylesheet.cssText
      elif do_jsmin:
        data = jsmin(data)
      if (not re.search(r'text', mimetype)):
        contents = [data]
      else:
        contents.append(getInfoComment(index, url, from_cache, fetch_time) + data)
      index += 1
      
    now = datetime.datetime.utcnow()
    format = "%a, %d %b %Y 00:00:00 GMT"
    
    self.response.headers["Cache-Control"] = "public"
    # Expires in 24 hours
    self.response.headers["Expires"] = (now + datetime.timedelta(1)).strftime(format)
    # Last-Modified 1 year ago
    self.response.headers["Last-Modified"] = (now + datetime.timedelta(-365)).strftime(format)
    
    resp = string.join(contents, '\n')
    if (output == "json"):
      resp = simplejson.dumps(resp)
      if (callback != ""):
          resp = "%s(%s)" % (callback, resp)
    self.response.out.write(resp)

def getMillis(delta):
  return (delta.seconds * 1000) + (delta.microseconds / 1000)

def getInfoComment(index, url, from_cache, fetch_time):
  source = "CACHE"
  if not from_cache:
    source = "FETCH=%dms" % fetch_time
  return "\n/* --- URL %d: %s [%s] --- */\n" % (index, url, source)

def main():
  application = webapp.WSGIApplication([(r'^/url/(.+)', MainHandler),
                                        ('/', MainHandler)],
                                       debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()
